package com.v9.common.core.service;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.v9.common.core.domain.AbstractEntity;
import com.v9.common.core.domain.AbstractLongEntity;
import com.v9.common.core.domain.AbstractUidEntity;
import com.v9.common.dto.MapperPage;
import com.v9.common.dto.SortItem;
import com.v9.common.exception.ApplicationException;
import com.v9.common.utils.BeanCoper;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.JdbcType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Types;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author lunshi
 */
public abstract class AbstractService<M extends BaseMapper<T>, T extends AbstractEntity, ID extends Serializable> extends ServiceImpl<M, T> implements IAbstractService<T, ID> {

    @Resource
    protected JdbcTemplate jdbcTemplate;


    protected NamedParameterJdbcTemplate parameterJdbcTemplate;

    @Value("${app.model.prd}")
    private Boolean appModel;



    @PostConstruct
    protected void init() {
        if (!appModel) {
            parameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
            TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
            TableName annotation = entityClass.getAnnotation(TableName.class);
            //如果是视图表，则退出
            if (annotation.view()) {
                return;
            }
            if (!tableExist(tableInfo.getTableName())) {
                createTable(tableInfo);
            } else {
                uptableColumn(tableInfo);
//                buildIndex(tableInfo.getTableName());
            }
        }
    }

    protected void uptableColumn(TableInfo tableInfo) {
        List<TableFieldInfo> change = new ArrayList<>();
        List<TableFieldInfo> addField = new ArrayList<>();
        List<Map<String, Object>> mapList = jdbcTemplate.queryForList("" +
                "select column_name as name," +
                "data_type as type," +
                "CHARACTER_MAXIMUM_LENGTH as length," +
                "is_nullable as nullable," +
                "column_comment as common, " +
                "NUMERIC_PRECISION as pre, " +
                "NUMERIC_SCALE as scale " +
                "from information_schema.COLUMNS where TABLE_SCHEMA =(select database()) and TABLE_NAME ='" + tableInfo.getTableName() + "'");

        tableInfo.getFieldList().forEach(item -> {
            TableField annotation = item.getField().getAnnotation(TableField.class);
            boolean add = true;
            for (Map<String, Object> e : mapList) {
                ColumnInfo info = new ColumnInfo();
                info.setName(e.get("name").toString().toLowerCase());
                info.setCommon((String) e.get("common"));
                info.setType((String) e.get("type"));
                if (e.get("length") != null) {
                    info.setLength(((BigInteger) e.get("length")).intValue());
                }
                info.setNullAble(e.get("nullable").equals("NO") ? false : true);
                if (annotation != null && info.getNullAble().booleanValue() != annotation.nullable() && info.getName().equals(item.getColumn())) {
                    change.add(item);
                    return;
                }
                if (e.get("pre") != null) {
                    info.setPrecision(((BigInteger) e.get("pre")).intValue());
                }
                if (e.get("scale") != null) {
                    info.setScale(((BigInteger) e.get("scale")).intValue());
                }
                if (info.getName().equals(item.getColumn())) {
                    add = false;

                    String type = null;
                    Integer length = null;
                    JdbcType jdbcType=null;
                    if(annotation!=null) {
                        length = annotation.length();
                        jdbcType = annotation.jdbcType();
                    }

                    if (item.getField().getType() == String.class && jdbcType == null) {
                        jdbcType = JdbcType.VARCHAR;
                        length = 32;
                    }
                    if (item.getField().getType() == Long.class && jdbcType == null) {
                        jdbcType = JdbcType.BIGINT;
                    }
                    if (item.getField().getType() == BigDecimal.class && jdbcType == null) {
                        jdbcType = JdbcType.NUMERIC;
                    }
                    if (item.getField().getType() == Date.class && jdbcType == null) {
                        jdbcType = JdbcType.DATE;
                    }
                    if (item.getField().getType() == Integer.class && jdbcType == null) {
                        jdbcType = JdbcType.INTEGER;
                    }
                    if (item.getField().getType() == Boolean.class && jdbcType == null) {
                        jdbcType = JdbcType.TINYINT;
                    }
                    if (jdbcType == JdbcType.UNDEFINED && item.getField().getType() == String.class) {
                        type = "varchar";
                        if (length.intValue() != info.length) {
                            change.add(item);
                            return;
                        }
                    }
                    if (jdbcType == JdbcType.CLOB) {
                        type = "text";
                        length = null;
                        if (info.getType().equals(type)) {
                            return;
                        }
                    }
                    if (item.getField().getType().isEnum()) {
                        type = "varchar";
                        length = 32;
                        if (info.getType().equals(type)) {
                            return;
                        }
                    }
                    if (item.getField().getType() == Boolean.class) {
                        type = "tinyint";
                        if (info.getType().equals(type)) {
                            return;
                        }
                    }
                    if (item.getField().getType() == Long.class) {
                        type = "bigint";
                        if (info.getType().equals(type)) {
                            return;
                        }
                    }
                    if (item.getField().getType() == Integer.class) {
                        type = "integer";
                        if (info.getType().equals(type)) {
                            return;
                        }
                    }
                    if (item.getField().getType() == Date.class) {
                        type = "datetime";
                        if (info.getType().equals(type)) {
                            return;
                        }
                    }
                    if (!info.getType().equals(type)) {
                        change.add(item);
                        return;
                    }
                }
            }
            if (add) {
                addField.add(item);
            }

        });
        List<String> sqls = new ArrayList<>();
        addField.forEach(item -> {
            StringBuilder stringBuilder = buildColumnStr(item);
            String sql = "ALTER TABLE `" + tableInfo.getTableName() + "` ADD COLUMN " + stringBuilder.toString();
            sqls.add(sql);
        });
        change.forEach(item -> {
            StringBuilder stringBuilder = buildColumnStr(item);
            String sql = "ALTER TABLE `" + tableInfo.getTableName() + "` CHANGE COLUMN `" + item.getColumn() + "` " + stringBuilder.toString();
            sqls.add(sql);
        });
        if (sqls.size() > 0) {
            jdbcTemplate.batchUpdate(sqls.toArray(new String[sqls.size()]));
        }
    }

    protected void createTable(TableInfo info) {

        StringBuilder stringBuilder = new StringBuilder("CREATE TABLE `").append(info.getTableName()).append("`  (\r");
        if (info.getKeyType() == Long.class) {
            stringBuilder.append("`id` bigint NOT NULL AUTO_INCREMENT COMMENT '标识',\r");
        }
        if (info.getKeyType() == String.class) {
            stringBuilder.append("`id` varchar(32) NOT NULL  COMMENT '标识',\r");
        }
        for (TableFieldInfo item : info.getFieldList()) {
            if (item.getColumn().equals("id")) {
                continue;
            }
            StringBuilder columnStr = buildColumnStr(item);
            stringBuilder.append(columnStr);
            stringBuilder.append(",");
        }
        stringBuilder.delete(stringBuilder.length() - 1, stringBuilder.length() - 1);
        stringBuilder.append("\r");
        stringBuilder.append("PRIMARY KEY (`id`)\r");
        stringBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;");

        log.error(stringBuilder.toString());
        jdbcTemplate.execute(stringBuilder.toString());
    }

    private StringBuilder buildColumnStr(TableFieldInfo item) {
        String type = null;
        Integer length = null;
        Integer precision = null;
        Boolean nullAble = Boolean.TRUE;
        String common = null;
        Field field = item.getField();
        JdbcType jdbcType = null;
        TableField annotation = field.getAnnotation(TableField.class);
        if (annotation != null) {
            length = annotation.length();
            common = annotation.common();
            nullAble = annotation.nullable();
            String numericScale = annotation.numericScale();
            jdbcType = annotation.jdbcType();
            if (jdbcType == JdbcType.UNDEFINED) {
                jdbcType = JdbcType.VARCHAR;
            }
            if (StringUtils.isNotBlank(numericScale)) {
                precision = Integer.parseInt(numericScale);
            } else {
                precision = 0;
            }
        }
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(" `").append(item.getColumn()).append("` ");
        if (item.getField().getType() == String.class && jdbcType == null) {
            jdbcType = JdbcType.VARCHAR;
            length = 32;
        }
        if (item.getField().getType() == Long.class && jdbcType == null) {
            jdbcType = JdbcType.BIGINT;
        }
        if (item.getField().getType() == BigDecimal.class && jdbcType == null) {
            jdbcType = JdbcType.NUMERIC;
        }
        if (item.getField().getType() == Date.class && jdbcType == null) {
            jdbcType = JdbcType.DATE;
        }
        if (item.getField().getType() == Integer.class && jdbcType == null) {
            jdbcType = JdbcType.INTEGER;
        }
        if (item.getField().getType() == Boolean.class && jdbcType == null) {
            jdbcType = JdbcType.TINYINT;
        }
        if (jdbcType == JdbcType.VARCHAR && !item.getField().getType().isEnum() && item.getField().getType() == String.class) {
            type = "varchar";
            if (length == 255) {
                length = 32;
            }
            stringBuilder.append(" varchar(").append(length).append(") ");
        }
        if (jdbcType == JdbcType.CLOB) {
            type = "text";
            stringBuilder.append(type);
        }

        if (item.getField().getType() == Boolean.class) {
            type = "tinyint";
            stringBuilder.append(" tinyint(4) ");
        }
        if (item.getField().getType().isEnum()) {
            type = "varchar";
            length = 32;
            stringBuilder.append(" varchar(").append(length).append(") ");
        }
        if (item.getField().getType() == Integer.class) {
            type = "int";
            stringBuilder.append(" int ");
        }
        if (item.getField().getType() == Long.class) {
            type = "bigint";
            stringBuilder.append(" bigint ");
        }
        if (item.getField().getType() == BigDecimal.class) {
            type = "decimal";
            if (precision == 0) {
                precision = 6;
            }
            stringBuilder.append(" decimal(").append(16).append(",").append(precision).append(") ");
        }
        if (item.getField().getType() == Date.class) {
            stringBuilder.append(" datetime ");
        }
        if ("varchar".equals(type)) {
            stringBuilder.append(" CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ");
        }
        if (!nullAble) {
            stringBuilder.append(" NOT NULL");
        }

        if (StringUtils.isNotBlank(common)) {
            stringBuilder.append(" COMMENT '").append(common).append("' ");
        }
        return stringBuilder;
    }

    protected boolean tableExist(String name) {
        Map<String, Object> map = jdbcTemplate.queryForMap("select database() as name");
        String dbName = (String) map.get("name");
        String isTableSql = String.format("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE " + "table_name = '%s' and table_schema='" + dbName + "'", name);
        map = jdbcTemplate.queryForMap(isTableSql);
        Long count = (Long) map.get("count");
        if (count > 0) {
            return true;
        }

        return false;
    }

    @Override
    public T findById(ID id) {
        T t = baseMapper.selectById(id);
        return t;
    }

    @Override
    public List<T> findByIds(ID... ids) {
      return baseMapper.selectBatchIds(List.of(ids));
    }

    @Override
    public T findByOne(T entity) {
        QueryWrapper<T> query = buildQueryWrapper(entity);
        List<T> query1 = query(query);
        if (query1 == null || query1.size() == 0) {
            return null;
        }
        return query1.get(0);
    }


    @Override
    public T findByOne(QueryWrapper<T> query) {
        query.eq("del_status", 0);
        return baseMapper.selectOne(query);
    }

    @Override
    public Long selectCount(QueryWrapper<T> query) {
        query.eq("del_status", 0);
        return baseMapper.selectCount(query);
    }


    @Override
    public Long selectCount(T query) {
        QueryWrapper<T> tQueryWrapper = buildQueryWrapper(query);
        return selectCount(tQueryWrapper);
    }

    @Override
    public void insert(T... t) {
        for (T item : t) {
            Date createTime = new Date();
            item.setCreateTime(createTime);
            item.setUpdateTime(createTime);
            item.setDelStatus(false);
            baseMapper.insert(item);
        }
    }

    @Override
    public void updateById(T... t) {
        List<T> types = new ArrayList<>();
        for (T item : t) {
            types.add(item
            );
            updateBatchById(types);
        }
    }


    @Override
    public void save(ID id, String property, Object value) {
        if (id == null) {
            throw new ApplicationException("根据id更新属失败,id不能为空");
        }
        QueryWrapper<T> queryWrapper = new QueryWrapper<T>();
        queryWrapper.eq("id", id);
        T instance = null;
        try {
            instance = getEntityClass().newInstance();
            TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
            tableInfo.setPropertyValue(instance, property, value);
        } catch (Exception e) {
            log.error("data.error：" + getEntityClass().getSimpleName() + " 更新属性" + property + "失败");
            throw new ApplicationException("data.error");
        }
        baseMapper.update(instance, queryWrapper);
    }

    @Override
    public void inc(ID id, String property, Number value) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
        List<TableFieldInfo> collect = tableInfo.getFieldList().stream().filter(item -> item.getProperty().equals(property)).collect(Collectors.toList());
        if (collect.size() != 1) {
            throw new ApplicationException("entity." + entityClass.getName() + " no property:" + property);
        }
        String column = collect.get(0).getColumn();
        String sql = "update " + tableInfo.getTableName() + " set " + column + "=" + column + "+" + value + " where id=" + id;
        int update = jdbcTemplate.update(sql);
        if (update != 1) {
            throw new ApplicationException("data.inc.error");
        }
    }

    @Override
    public void inc(ID id, Map<String, Number> incMap) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
        StringBuilder  builder=new StringBuilder("update " + tableInfo.getTableName() + " set ");
        incMap.forEach((k,v)->{
            List<String> columns = tableInfo.getFieldList().stream().filter(item -> item.getProperty().equals(k)).map(item -> item.getColumn()).collect(Collectors.toList());
            if(columns.size()==0){
                throw new ApplicationException(entityClass.getSimpleName()+" inc error, property "+k+" not exist");
            }
            String column=columns.get(0);
            builder.append(column).append("=").append(column).append("+").append(v);
        });
        builder.append(" where id=?");
        String sql = builder.toString();
        int[] argTypes=null;
        if(id instanceof Number){
            argTypes=new int[Types.BIGINT];
        }
        else{
            argTypes=new int[Types.NVARCHAR];
        }
        int update = jdbcTemplate.update(sql,argTypes,new Object[]{id});
        if (update != 1) {
            throw new ApplicationException("data.inc.error");
        }
    }

    @Override
    public void remove(ID... ids) {
        remove(false, ids);
    }

    @Override
    public void remove(Boolean soft, ID... ids) {
        if (!soft) {
            for (ID item : ids) {
                baseMapper.deleteById(item);
            }
            return;
        }
        for (ID item : ids) {
            removeById(item, false);
        }
    }

    @Override
    public void remove(QueryWrapper<T> query) {
        baseMapper.delete(query);
    }

    @Override
    public List<T> queryAll() {
        return baseMapper.selectList(null);
    }

    @Override
    public <T> List<T> query(String sql, Map<String, ?> paramMap, Class<T> elementType) {

        return parameterJdbcTemplate.queryForList(sql, paramMap, elementType);

    }

    @Override
    public List<Map<String, Object>> query(String sql, Map<String, Object> params) {
        return parameterJdbcTemplate.queryForList(sql, params);
    }

    @Override
    public List<T> query(QueryWrapper<T> query) {
        if (query == null) {
            return queryAll();
        }
        return baseMapper.selectList(query);
    }

    @Override
    public List<T> query(T entity) {
        if (entity == null) {
            return queryAll();
        }
        QueryWrapper<T> query = buildQueryWrapper(entity);
        return query(query);
    }

    /**
     * 构建查询对像*
     *
     * @param entity
     * @return
     */
    private QueryWrapper<T> buildQueryWrapper(T entity) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
        QueryWrapper<T> query = new QueryWrapper<>();
        tableInfo.getFieldList().forEach(item -> {
            Object property = BeanUtil.getProperty(entity, item.getProperty());
            if (property != null) {
                query.eq(item.getColumn(), property);
            }
        });
        return query;
    }

    @Override
    public T saveOrInsert(T t) {
        if (t instanceof AbstractLongEntity) {
            AbstractLongEntity uidEntity = (AbstractLongEntity) t;
            if (uidEntity.getId() != null) {
                updateById(t);
                return t;
            }
            insert(t);
            return t;
        }
        if (t instanceof AbstractUidEntity) {
            AbstractUidEntity uidEntity = (AbstractUidEntity) t;
            if (uidEntity.getId() != null) {
                updateById(t);
                return t;
            }
            insert(t);
            return t;
        }
        throw new ApplicationException("saveOrInsert must be " + AbstractLongEntity.class.getSimpleName() + " or " + AbstractUidEntity.class.getSimpleName() + "  type");
    }

    @Override
    public void update(UpdateWrapper<T> updateWrapper) {
        baseMapper.update(null, updateWrapper);
    }

    @Override
    public Page<T> queryByPage(T entity, Pageable pageable) {
        QueryWrapper<T> query = buildQueryWrapper(entity);
        return queryByPage(query, pageable);
    }

    @Override
    public Page<T> queryByPage(QueryWrapper<T> query, Pageable pageable) {
        Long aLong = baseMapper.selectCount(query);
        com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageable.getPageNumber(), pageable.getPageSize());
        com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> tPage = baseMapper.selectPage(page, query);
        PageRequest of = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize());
        return new PageImpl(tPage.getRecords(), of, aLong);
    }

    /**
     * 表信息
     */
    @Data
    private class ColumnInfo {
        private String name;
        private Boolean nullAble;
        private String type;
        private Integer length;
        private String common;
        private Integer precision;
        private Integer scale;
    }

    /**
     * 对象转换
     *
     * @param typeClass 目标类型
     * @param source    源类型
     * @param <T>
     * @return
     */
    protected <T> T clone(Class<T> typeClass, Object source) {
        return BeanCoper.copyProperties(typeClass, source);
    }

    /**
     * list数据转换
     *
     * @param typeClass  目标类型
     * @param sourcepage 源类型
     * @param <T>
     * @return
     */
    protected <T> List<T> clone(Class<T> typeClass, List<?> sourcepage) {
        return BeanCoper.copyList(typeClass, sourcepage);
    }

    /**
     * 分页数据转换
     *
     * @param typeClass  目标类型
     * @param sourcepage 源类型
     * @param <T>
     * @return
     */
    protected <T> Page<T> clone(Class<T> typeClass, Page<?> sourcepage) {
        return BeanCoper.copyPage(typeClass, sourcepage);
    }


    protected Map<String, Object> toPage(Map<String, Object> params) {
        return toPage(params, null);
    }


    protected Map<String, Object> toPage(Pageable page) {
        return toPage(new HashMap<>(), page);
    }

    protected Map<String, Object> toPage(Map<String, Object> params, Pageable page) {
        MapperPage mapperPage = new MapperPage();
        List<SortItem> orders = new ArrayList<>();
        mapperPage.setOrders(orders);
        if (page != null) {
            Sort sort = page.getSort();
            Iterator<Sort.Order> iterator = sort.stream().iterator();
            while (iterator.hasNext()) {
                Sort.Order next = iterator.next();
                SortItem item = new SortItem();
                item.setName(next.getProperty());
                item.setType(Sort.Direction.DESC.name());
                orders.add(item);
            }
            mapperPage.setOffset(Long.valueOf(page.getOffset()));
            mapperPage.setLimit(page.getPageSize());
            params.put("page", mapperPage);
        }
        params.put("query", params);

        return params;
    }

    protected String getColumnToField(String property) {
        return BeanCoper.getColumnToField(property);
    }
}
